Skip to content
字数
2019 字
阅读时间
8 分钟

虚拟 DOM

virtual DOM 是将真实的 DOM 的数据抽取出来,以对象的形式模拟树形结构。比如 dom 是这样的:

本质就是在 JS 和 DOM 之间做了一个缓存

javascript
<div>
    <p>123</p>
</div>

对应的 virtual DOM(伪代码):

javascript
var Vnode = {
    tag: 'div',
    children: [
        { tag: 'p', text: '123' }
    ]
};

所谓虚拟 DOM 其实只是一个包含了标签类型 type,属性 props 以及它包含子元素 children 的 js 对象

虚拟 DOM 好处

  1. 是以 javascript 对象为基础而不依赖真实平台环境,具有跨平台的能力
    • 浏览器平台渲染 DOM
    • 服务端渲染 SSR (Nuxt.js/Next.js)
    • 原生应用 (Weex/React Native)
    • 小程序 (mpvue/uni-app) 等
  2. 复杂视图情况下提升渲染性能(原生 DOM 操作慢,可以将 DOM 操作放在 JS 层,利用 patch 等算法计算真正需要更新的节点,减少 DOM 操作,提高效率)
  3. 维护视图和状态的关系(虚拟 dom 可以很好地跟踪当前 dom 状态,因为它会根据当前数据生成一个描述当前 dom 结构的虚拟 dom,然后数据发生变化时,有生成一个新的虚拟 dom,而两个虚拟 dom 恰好保存了变化前后的状态)

key 的作用

在组件进行 diff 时作为唯一标识

用 index 作为 key 的问题

  1. 若对数据进行:逆序添加、逆序删除等破防顺序的操作:
    • 会产生没有必要的真实 DOM 更新 ——> 界面效果没问题,但效率低
  2. 如果结构中还包含输入类的 DOM:
    • 会产生错误 DOM 更新 ——> 界面有问题
  3. 仅用于列表展示,使用 index 作为 key 是没有问题的

Diff 算法

彻底搞懂 Vue 把虚拟 dom(vnode)转化为真实 dom 的过程 (diff.js)

对同层的树节点进行比较(由上至下,层层对比),时间复杂度 O(n)

比较点:

  1. 比较节点类型和节点属性是否相同
  2. 再比较子节点或者文本节点是否相同
    • 文本节点先比较节点内容类型(String 还是 Number)
    • 子节点比较调用 patchVNode 方法

以上出现一个不匹配则判断节点不同,直接用 VNode 替换

patchVnode 方法

  • oldVnode 有子节点,Vnode 没有
  • oldVnode 没有子节点,Vnode 有
  • 都只有文本节点
  • 都有子节点(调用 updateChildren 方法

updateChildren 方法 (子节点更新策略)

详解 vue 的 diff 算法

Vue 虚拟 DOM 实现原理

子节点更新策略

四种命中查找,由上至下依次命中:(待查找的节点的头尾节点指针)

  1. 新前和旧前(相同则双方指针下移,重新比较,否则 判断下移)
  2. 新后和旧后(相同则双方指针上移动)
  3. 新后和旧前(相同则新后指针上移,旧前指针下移)
  4. 新前和旧后(相同则新前指针下移,旧后指针上移)

循环的条件: while(新前 <= 新后 && 旧前 <= 旧后)

循环结束后:

  • 如果新节点存在节点,则新增节点
  • 如果旧节点存在节点,则删除 旧前旧后 间的节点

第三种情况发送

旧节点设为 undefined 新前指向的节点,复制到旧后之后,

第四种情况发生

旧节点设为 undefined,新节点指向的节点,复制到旧前之前

四种情况都没发生

判断 新前指针指向的节点在 old 中有没有

有就将该节点插入到 oldStart 前面,将 old 中该节点值设置为 undefinednewStart 指针 下移

如果没有就将该节点插入到 old 之前,newStart 指针 下移

patch 函数

两个作用:

  • 根据 VNode 挂载 DOM
  • 根据新旧 vnode 更新 DOM

patch 函数入参。

  • 第一个参数 n1 表示旧的 vnode,当 n1 为 null 的时候,表示是一次挂载的过程
  • 第二个参数 n2 表示新的 vnode 节点,后续会根据这个 vnode 进行相关的处理
  • 第三个参数 container 表示 DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面

vue2 和 vue3 diff 算法区别

vue2 的双端比较

vue3 的最长递增子序列

  1. 从前往后对比 (头部与头部比较)
  2. 从后往前对比 (尾部与尾部比较)
  3. 基于最长子序列的比较进行 -》移动|添加|删除

举个例子:

旧数组:【a,b,c,d,e,f,g】
新数组:【a, b, f, c, d, e, h, g]

**首先是开始最简单的比较获取之前的缓存数据:**
1.  首先是头头对比,发现不同就停止本次循环【a,b】
2.  然后是尾尾比较得到【g】;

**经过1.2的比较后得到不同局部数据【f,c,d,e,h】,接下来就是不同数据的比较,这一步就是要使用最长子序列方法进行(移动|添加|删除)的操作。**
**在源码中使用map函数对数组的值和key进行一个绑定。**(有兴趣的同学可以看下源码)

源码中通过 newIndexToOldIndexMap 拿到在数组里对应的下标,生成数组 [ 5, 2, 3, 4, -1 ],-1 是老数组里没有的,没有的数据我们只能往旧节点补充数据。

很明显可以发现在[ 5, 2, 3, 4, -1 ]中【2,3,4】是有序的,**并且是依次递增的,此时他对应的节点是【c,d,e】,这个时候我们就很容易的保持【2,3,4】不变进行其他数据的移位,既是在此基础的前方插上一位就能完成新节点的更新**

vue3 中对 diff 算法的优化

Vue3中的diff算法优化 - 掘金

  1. 节点标记为动态和静态节点
  2. 数组循环算法调整为 最小递增子序列
  3. 事件缓存

vue2 中的虚拟 dom 是进行全量的对比,当页面数据发生变更后,会判断虚拟 don 所有的节点有没有发生变化

针对这一点,vue3 在创建新的虚拟 dom 树的时候,会在动态标签(这个 dom 中的内容可能会发生变化)后添加一个静态标记,在与上次虚拟 dom 树比较时就只对比这些带有静态标记的节点

事件缓存

比如这样一个有点击事件的按钮

js
<button @click="handleClick">按钮</button>
复制代码

来看下在 Vue3 被编译后的结果

js
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("button", {
    onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
  }, "按钮"))
}

VNode 生成真实节点树

彻底搞懂 Vue 把虚拟 dom(vnode)转化为真实 dom 的过程 (render.js)

vnode 到真实 DOM 是如何转变的

通过 render 将 VNode 生产 DOM,涉及三个方法 render,createRealElement 以及 setProperties

主要进行以下步骤:

  1. 判断 VNode 类型,如果是组件类型,则挂载组件(patch 方法)
    1. 创建组件实例
    2. 渲染组件子树(调用 render 函数得到一个子树的 VNode)
    3. 挂载到 container 中(DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面)
      2.如果是普通元素,也调用 patch 方法
    4. 创建 DOM 元素节点
    5. 处理 props 属性
    6. 处理文本或者数组的子节点
    7. 递归 patch 子节点(深度优先遍历树的方式)

流程图如下:

参考

官网: API — Vue.js

贡献者

The avatar of contributor named as jiechen jiechen

页面历史

撰写